package com.origin.chaos.security.serivce.impl;

import com.alibaba.fastjson.JSONObject;
import com.origin.chaos.common.bean.vo.Result;
import com.origin.chaos.common.exception.OutPutMsgException;
import com.origin.chaos.common.util.ObjectUtil;
import com.origin.chaos.mail.MailService;
import com.origin.chaos.security.bean.LoginUser;
import com.origin.chaos.security.bean.entity.User;
import com.origin.chaos.security.bean.vo.*;
import com.origin.chaos.security.serivce.IUserService;
import com.origin.chaos.security.util.CodeUtil;
import com.origin.chaos.security.util.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

/**
 * @author: tao.ding
 * @date 2021-07-16
 */
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
    /**
     * 用户ID计数器
     */
    private static final String USER_ID_COUNTER = "user_id_counter";

    /**
     * 用户邮箱Hash key
     */
    private static final String USER_EMAIL_HASH_KEY = "accounts";

    /**
     * 用户信息Hash key前缀
     */
    private static final String USER_INFO_HASH_KEY_PREFIX = "user:";

    /**
     * 登录信息hash key
     */
    private static final String USER_LOGIN_INFO_HASH_KEY = "login_info";

    /**
     * 登录token hash key
     */
    private static final String TOKEN_INFO_HASH_KEY = "token_info";

    /**
     * 每日活跃用户数记录
     */
    private static final String USER_LOGIN_INFO_BITMAP_KEY = "login_log";

    /**
     * 注册验证码缓存KEY，后面需要拼接上用户的注册邮箱
     */
    private static final String REGISTER_CODE_KEY = "register_code:";

    /**
     * 发送注册验证码的间隔时间KEY，后面需要拼接上用户的注册邮箱
     */
    private static final String REGISTER_CODE_SEND_IDLE_KEY = "register_code_send_idle:";

    /**
     * 重置密码验证码缓存KEY，后面需要拼接上用户的注册邮箱
     */
    private static final String RESET_PSD_CODE_KEY = "reset_psd_code:";

    /**
     * 发送重置密码验证码的间隔时间KEY，后面需要拼接上用户的注册邮箱
     */
    private static final String RESET_PSD_CODE_SEND_IDLE_KEY = "reset_psd_code_send_idle:";

    /**
     * 验证码失效时间5分钟
     */
    private static final long CODE_EXPIRE_SECONDS = 300;

    /**
     * 重发验证码间隔时间1分钟
     */
    private static final long CODE_SEND_IDLE_SECONDS = 60;

    private StringRedisTemplate stringRedisTemplate;

    private MailService mailService;

    @Override
    public Result<Long> sendRegisterCode(String email) {
        if (this.containsAccount(email)) {
            throw new OutPutMsgException("账号已存在");
        }
        final String codeKey = REGISTER_CODE_KEY + email;
        final String idleKey = REGISTER_CODE_SEND_IDLE_KEY + email;

        if (ObjectUtil.isTrue(this.stringRedisTemplate.hasKey(codeKey))) {
            long idleTime = ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey));
            if (idleTime > 0) {
                return Result.noEffect(ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey)));
            }
        }
        String randomCode = CodeUtil.randomIntCode(6);
        this.mailService.sendRegisterCode(email, randomCode);
        this.stringRedisTemplate.opsForValue().set(codeKey, randomCode, Duration.ofSeconds(CODE_EXPIRE_SECONDS));
        this.stringRedisTemplate.opsForValue().set(idleKey, "", Duration.ofSeconds(CODE_SEND_IDLE_SECONDS));
        return Result.ofBody(ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey)));
    }

    @Override
    public Result<Long> sendResetPsdCode(String email) {
        if (!this.containsAccount(email)) {
            throw new OutPutMsgException("账号不存在");
        }
        final String codeKey = RESET_PSD_CODE_KEY + email;
        final String idleKey = RESET_PSD_CODE_SEND_IDLE_KEY + email;

        if (ObjectUtil.isTrue(this.stringRedisTemplate.hasKey(codeKey))) {
            long idleTime = ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey));
            if (idleTime > 0) {
                return Result.noEffect(ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey)));
            }
        }
        String randomCode = CodeUtil.randomIntCode(6);
        this.mailService.sendResetPsdCode(email, randomCode);
        this.stringRedisTemplate.opsForValue().set(codeKey, randomCode, Duration.ofSeconds(CODE_EXPIRE_SECONDS));
        this.stringRedisTemplate.opsForValue().set(idleKey, "", Duration.ofSeconds(CODE_SEND_IDLE_SECONDS));
        return Result.ofBody(ObjectUtil.ofLong(this.stringRedisTemplate.getExpire(idleKey)));
    }

    @Override
    public UserInfoVo register(RegisterVo vo) {
        if (this.containsAccount(vo.getEmail())) {
            throw new OutPutMsgException("账号已存在");
        }
        final String codeKey = REGISTER_CODE_KEY + vo.getEmail();
        if (!vo.getRegisterCode().equals(this.stringRedisTemplate.opsForValue().get(codeKey))) {
            throw new OutPutMsgException("验证码不正确");
        }

        Long nextId = this.stringRedisTemplate.opsForValue().increment(USER_ID_COUNTER);
        if (null == nextId) {
            throw new OutPutMsgException("从Redis中获取用户递增ID失败");
        }
        this.stringRedisTemplate.opsForHash().put(USER_EMAIL_HASH_KEY, vo.getEmail(), nextId + "");
        User user = new User();
        user.setId(nextId);
        user.setNickName(vo.getNickName());
        user.setEmail(vo.getEmail());
        user.setPassword(vo.getPassword());
        this.stringRedisTemplate.opsForHash().putAll(USER_INFO_HASH_KEY_PREFIX + nextId, (JSONObject) JSONObject.toJSON(user));
        // 删除验证码
        this.stringRedisTemplate.delete(codeKey);
        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setNickName(vo.getNickName());
        return userInfoVo;
    }

    @Override
    public UserInfoVo resetPsd(ResetPsdVo vo) {

        Long userId = Long.parseLong(Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(USER_EMAIL_HASH_KEY, vo.getEmail())).orElse("0").toString());
        if (0 == userId) {
            throw new OutPutMsgException("账号不存在");
        }
        final String codeKey = RESET_PSD_CODE_KEY + vo.getEmail();
        if (!vo.getResetPsdCode().equals(this.stringRedisTemplate.opsForValue().get(codeKey))) {
            throw new OutPutMsgException("验证码不正确");
        }

        this.stringRedisTemplate.opsForHash().put(USER_INFO_HASH_KEY_PREFIX + userId, "password", vo.getPassword());
        // 删除验证码
        this.stringRedisTemplate.delete(codeKey);
        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setNickName(Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(USER_INFO_HASH_KEY_PREFIX + userId, "nickName")).orElse("").toString());
        return userInfoVo;
    }

    @Override
    public UserInfoVo changeInfo(ChangeInfoVo vo) {
        LoginUser loginUser = TokenUtil.getCurrentLoginUser();
        this.stringRedisTemplate.opsForHash().put(USER_INFO_HASH_KEY_PREFIX + loginUser.getId(), "nickName", vo.getNickName());
        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setId(loginUser.getId());
        userInfoVo.setNickName(vo.getNickName());
        return userInfoVo;
    }

    @Override
    public String login(LoginVo vo) {
        if (!this.containsAccount(vo.getEmail())) {
            throw new OutPutMsgException("用户不存在");
        }
        String userId = (String) Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(USER_EMAIL_HASH_KEY, vo.getEmail())).orElse("0");
        // 判断是否禁止登录
        final String loginForbiddenKey = "login_forbidden:" + userId;
        Boolean forbidden = this.stringRedisTemplate.hasKey(loginForbiddenKey);
        if (null != forbidden && forbidden) {
            throw new OutPutMsgException("禁止重复尝试, 请" + Optional.ofNullable(this.stringRedisTemplate.getExpire(loginForbiddenKey)).orElse(0L) + "秒后重试");
        }

        String userInfoHashKey = USER_INFO_HASH_KEY_PREFIX + userId;
        final String failedHashKey = "login_failed_times";
        if (!vo.getPassword().equals(Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(userInfoHashKey, "password")).orElse(""))) {
            // 登录失败记录失败次数

            int failedTimes = Integer.parseInt((String) Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(failedHashKey, userId)).orElse("0"));
            this.stringRedisTemplate.opsForHash().put(failedHashKey, userId, ++failedTimes + "");
            if (failedTimes > 3) {
                this.stringRedisTemplate.opsForValue().set(loginForbiddenKey, "", Duration.ofMinutes(1));
            }
            throw new OutPutMsgException("密码错误");
        }
        String oldToken = (String) Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(USER_LOGIN_INFO_HASH_KEY, userId)).orElse("");
        if (!ObjectUtils.isEmpty(oldToken)) {
            this.stringRedisTemplate.opsForHash().delete(TOKEN_INFO_HASH_KEY, oldToken);
        }
        String token = UUID.randomUUID().toString();
        this.stringRedisTemplate.opsForHash().put(USER_LOGIN_INFO_HASH_KEY, userId, token);
        this.stringRedisTemplate.opsForHash().put(TOKEN_INFO_HASH_KEY, token, userId);
        this.stringRedisTemplate.delete(loginForbiddenKey);
        this.stringRedisTemplate.opsForHash().delete(failedHashKey, userId);
        this.stringRedisTemplate.opsForValue().setBit(
                USER_LOGIN_INFO_BITMAP_KEY + new SimpleDateFormat("yyyy-MM-dd").format(new Date()),
                Long.parseLong(userId),
                true);
        return token;
    }

    @Override
    public UserInfoVo getUserInfo() {
        LoginUser loginUser = TokenUtil.getCurrentLoginUser();
        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setId(loginUser.getId());
        userInfoVo.setNickName(loginUser.getNickName());
        return userInfoVo;
    }

    @Override
    public void logout() {
        String token = TokenUtil.getCurrentLoginUserToken();
        String userId = (String) this.stringRedisTemplate.opsForHash().get(TOKEN_INFO_HASH_KEY, token);
        if (ObjectUtils.isEmpty(userId)) {
            return;
        }
        this.stringRedisTemplate.opsForHash().delete(TOKEN_INFO_HASH_KEY, token);
        this.stringRedisTemplate.opsForHash().delete(USER_LOGIN_INFO_HASH_KEY, userId);
    }

    @Override
    public LoginUser getLoginUserByToken(String token) {
        String userId = (String) this.stringRedisTemplate.opsForHash().get(TOKEN_INFO_HASH_KEY, token);
        if (ObjectUtils.isEmpty(userId)) {
            return null;
        }
        String userInfoHashKey = USER_INFO_HASH_KEY_PREFIX + userId;
        Map<Object, Object> userInfoMap = this.stringRedisTemplate.opsForHash().entries(userInfoHashKey);
        if (userInfoMap.size() == 0) {
            return null;
        }
        LoginUser loginUser = new LoginUser();
        loginUser.setId(Long.parseLong(userId));
        loginUser.setEmail(userInfoMap.getOrDefault("email", "").toString());
        loginUser.setNickName(userInfoMap.getOrDefault("nickName", "").toString());
        return loginUser;
    }

    private boolean containsAccount(String email) {
        return this.stringRedisTemplate.opsForHash().hasKey(USER_EMAIL_HASH_KEY, email);
    }

    @Override
    public JSONObject manageUserInfo() {
        JSONObject result = new JSONObject();
        result.put("users", this.stringRedisTemplate.opsForHash().size(USER_EMAIL_HASH_KEY));
        result.put("todayActivate", this.stringRedisTemplate.execute(new RedisCallback() {

            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.bitCount((USER_LOGIN_INFO_BITMAP_KEY + new SimpleDateFormat("yyyy-MM-dd").format(new Date())).getBytes(StandardCharsets.UTF_8));
            }
        }));
        return result;
    }

    @Override
    public UserOpenInfoVo getUserOpenInfoById(long id) {
        final String userInfoKey = USER_INFO_HASH_KEY_PREFIX + id;
        if (ObjectUtil.isTrue(this.stringRedisTemplate.hasKey(userInfoKey))) {
            UserOpenInfoVo userOpenInfoVo = new UserOpenInfoVo();
            userOpenInfoVo.setId(id);
            userOpenInfoVo.setNickName(Optional.ofNullable(this.stringRedisTemplate.opsForHash().get(userInfoKey, "nickName")).orElse("未知用户").toString());
            return userOpenInfoVo;
        }
        return UserOpenInfoVo.anonymous(id);
    }

    @Autowired
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Autowired
    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }
}
